package weeny.pre.face.operation;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;

import prefuse.data.Tuple;
import prefuse.data.expression.BooleanLiteral;
import prefuse.data.expression.Expression;
import prefuse.data.expression.Predicate;
import weeny.pre.display.MoreVisualization;
import weeny.pre.face.ExpressionBox;
import weeny.pre.face.FilterBox;
import weeny.pre.face.operation.AbstractOperation.ComboItem;
import weeny.util.io.ModUtil;
import weeny.util.logs.Debug;

/**
 * Give a combo box to support customized operations, such as setting column
 * values, changeing focus groups, etc.
 * 
 * 1) Every operation should be added by addOperation(...). Everytime choosing
 *    item, it will call {@link AbstractOperation#itemInvoked(String, boolean)}
 *    to notify the corresponding operation. And then the operation should call
 *    {@link #updateDetail(Component)} to update "Customized Detail Component",
 *    so that the operation can get the user input for further process. The
 *    operation should be responsible for handling the event happening in the
 *    "Customized Detail Component" too. 
 * 2) Every operation should inherit {@link AbstractOperation}. You can find
 *    more details there.
 * 3) You can set a filter to narrow down the target of your operation. You can
 *    call either {@link #setFilter(FilterBox)} or {@link #setFilter(Predicate)}
 *    And then you can call {@link #filteredItems(boolean)} in your operation
 *    to iterate through all the filtered items.
 *    
 * The interface is looking like this:
 * 
 * +----------+--------------------+-----------------------------+
 * |  LABEL:  | Operation Combobox | Customized Detail Component |
 * +----------+--------------------+-----------------------------+
 * 
 * When necessary, you can call {@link #updateAllOperations()}, if something in
 * the operation combobox shoud be updated.
 * 
 * There is a infoLabel member you can {@code setInfoLabel(JLabel)} to register
 * an interface to show  such as the action result to people. And then you call 
 * {@link OperationBox#updateInfo(String)} in your {@link AbstractOperation}.
 * 
 * @author weeny
 *
 */
public class OperationBox extends Box{
	public static final String NO_OPERATION_INFO = " No operation performed";
	public static final JTextField DUMMY_DETAIL = new JTextField();
	static { 
		DUMMY_DETAIL.setText("No operation allowed");
		DUMMY_DETAIL.setEnabled(false); 
	}
	
	protected JComboBox operationCombo = new JComboBox();
	protected JLabel infoLabel = null;
	protected Component detailComponent = new JTextField();
	protected Map<String, AbstractOperation> operationMap = new LinkedHashMap();
	protected MoreVisualization vis = null;
	/**
	 * Only items meeting this filter will be chosen to perform the operation.
	 * By default, it is null (i.e., all items~)
	 */
	protected Predicate filter = null;
	/**
	 * Set a filter to this OperationBox, so that every action will be performed
	 * on those items after the filter
	 */
	public void setFilter(Predicate filter){
		this.filter = filter;
	}
	/**
	 * Set an expression box to control the filter, so that the filter will be
	 * automatically updated based on the parse reusult of the expression box
	 */
	public void setFilter(FilterBox filterBox){
		ActionListener filterListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				Expression exp = ((ExpressionBox)e.getSource()).getExpr();
				if ((exp instanceof Predicate)) {
					filter = (Predicate) exp;
				}
				else {
					Debug.prob("Not a Predicate, use FALSE instead");
					filter = BooleanLiteral.FALSE;					
				}
				Debug.info("Filter is changed to: "+exp);
				//filter is changed, we need to check this
				setFilter(filter);
			}
		};
		//we need to add a listener to the filter so we can automatically update
		//the filter
		filterBox.addActionListener(filterListener);
	}
	/**
	 * Set a JLabel for displaying action statistic info.
	 * @see OperationBox#updateInfo(String)
	 */
	public void setInfoLabel(JLabel label){
		this.infoLabel = label;
	}
	
	/**
	 * Get the iterator for every item meeting the {@link #filter}. If the
	 * filter means *all items*, a dialog will show up to ask you to confirm.
	 * But if you are sure about what you are doing and want to ignore the
	 * dialog, you can set allowAll to be true to ignore the checking
	 */
	public Iterator<Tuple> filteredItems(boolean allowAll){
		if ((filter == BooleanLiteral.TRUE || filter == null) && !allowAll) {
			//if filter is (null or TRUE) and alt is not down
			String warningMsg = "Filter indicates all items, continue? \n";
			warningMsg += "(Hold Alt key to override the warning)";
			int option = JOptionPane.showConfirmDialog(
					null, warningMsg, "Warning", JOptionPane.YES_NO_OPTION); 
			if (option != JOptionPane.YES_OPTION) {
				//the action is canceled, return
				updateInfo(OperationBox.NO_OPERATION_INFO);
				return Collections.EMPTY_LIST.iterator();
			}
			else {
				Debug.info("Confirm to iterate all items!");
			}
		}
		//use the filter to get the iterator
		return vis.items(filter);
	}
	
	/**
	 * Get the visualization
	 * @return
	 */
	public MoreVisualization getVisualization(){
		return vis;
	}
	/**
	 * Get the Predicate that is used to filter the items
	 */
	public Predicate getFilter(){
		return filter;
	}

	/**
	 * Listener monitoring the operation combobox. When the combobox item is
	 * changed, the corresponding operation will be notified by calling
	 * {@link AbstractOperation#itemInvoked(String, boolean)}
	 */
	protected ActionListener itemChangeAction = new ActionListener() {
		public void actionPerformed(ActionEvent e) {
			ComboItem cItem = (ComboItem) operationCombo.getSelectedItem();
			AbstractOperation operation = operationMap.get(cItem.title);
			String item = cItem.item;
			operation.itemInvoked(item, ModUtil.isAltDown(e));
		}
	};
	
	public OperationBox(MoreVisualization vis) {
		super(BoxLayout.X_AXIS);
		this.vis = vis;

		operationCombo.setFocusable(false);
		operationCombo.setPreferredSize(new Dimension(100, 30));
		operationCombo.setMinimumSize(new Dimension(100, 30));
		operationCombo.addActionListener(itemChangeAction);
		
		operationCombo.setRenderer(new ListCellRenderer() {
			public Component getListCellRendererComponent(JList list, Object v,
					int index, boolean isSelected, boolean cellHasFocus) {
				if (v == null) {					
					return itemRenderer.getListCellRendererComponent(list, v, 
							index, isSelected, cellHasFocus);
				}
				v = ((ComboItem) v).item;
				if (!operationMap.containsKey(v.toString())) {
					//make sure the items are indented
					v = "  "+v;
				}
				return titleRenderer.getListCellRendererComponent(list, v, 
						index, isSelected, cellHasFocus);
			}
		});
		this.add(Box.createHorizontalStrut(5));
		this.add(new JLabel("Do:"));
		this.add(operationCombo);
		this.add(detailComponent);
	}
	
	//we have two different renderers for the combobox items. One for operation
	//title, and the other for the operation item 
	protected ListCellRenderer itemRenderer = operationCombo.getRenderer();
	protected ListCellRenderer titleRenderer = new DefaultListCellRenderer();
	
	/**
	 * Add one new operation to this box, duplicated title is forbidden. We also
	 * check if the operationBox in the operation is identical to itself 
	 */
	public void addOperation(AbstractOperation operation){
		if (operationMap.containsKey(operation.getTitle())) {
			throw new IllegalArgumentException("Duplicated operation name");
		}
		if (this != operation.getOperationBox()){
			//the operationBox in the operation should be equal to this!
			throw new IllegalArgumentException("They do not match!");
		}
		operationMap.put(operation.getTitle(), operation);
		operation.prepareCombo(operationCombo);
		updateAllOperations();
	}
	
	/**
	 * reset the operation combobox with all the registered operations.
	 */
	public void updateAllOperations(){
		operationCombo.removeActionListener(itemChangeAction);
		operationCombo.removeAllItems();
		for(String title : operationMap.keySet()){
			operationMap.get(title).prepareCombo(operationCombo);
		}
		operationCombo.addActionListener(itemChangeAction);
		operationCombo.revalidate();
		operationCombo.repaint();
	}
	
	/**
	 * called by Operation to show operation result
	 */
	public void updateInfo(String info){
		if (infoLabel != null) {
			infoLabel.setText(" "+info);
		}
		else {
			Debug.info("label is null, so ignore the msg: "+info);
		}
	}
	
	/**
	 * replace the detail component with the new one. if null is used, then we
	 * just show an invalid jtextfield as a place holder. This is just a ui
	 * update operation, so nothing is actually done. This function is usually
	 * called when we select a new combo item. Then the updated detail component
	 * should handle the real operation, such as changing values in the filtered
	 * visual table.
	 */
	public void updateDetail(Component newDetail){
		newDetail = newDetail == null? DUMMY_DETAIL : newDetail;
		//this is a new action, so no operation has been performed
		updateInfo(NO_OPERATION_INFO);
		remove(detailComponent);
		this.add(newDetail);
		detailComponent = newDetail;
		this.revalidate();
		this.repaint();
	}
}